package apapl;

import apapl.parser.*;
import apapl.data.*;
import java.util.*;
import apapl.program.Beliefbase;
import java.util.ArrayList;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.StringReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.LinkedList;
import java.io.StringBufferInputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.io.File;
import java.util.regex.Pattern;
import com.ugos.JIProlog.engine.JIPSyntaxErrorException;

/**
 * The convenience class for using the parsing functionality generated by javaCC as defined
 * in the package {@link apapl.parser}.
 */
public class Parser
{
	// the actual parser as generated by JavaCC
	private Parser2apl parser;
	
	/**
	 * Constructs a parser.
	 */
	public Parser()
	{
		parser = new Parser2apl(new StringReader(""));
	}

	/**
	 * Parses a string representing a function.
	 * 
	 * @param s the string representation of the function
	 * @return the parsed APLFunction object
	 * @throws ParseException
	 */
	public APLFunction parseAPLFunction(String s) throws ParseException
	{
		APLFunction a = null;
		parser.ReInit(new StringReader(s));
		a = parser.APLFunction();
		
		return a;
	}

	/**
	 * Parses a string representing a term.
	 * 
	 * @param s the string representation of the term
	 * @return the parsed Term object
	 * @throws ParseException
	 */
	public Term parseTerm(String s) throws ParseException
	{
		Term t = null;

		parser.ReInit(new StringReader(s));
		t = parser.Term();
		
		return t;
	}
	
	/**
	 * Parses a string representing a query.
	 * 
	 * @param s the string representation of the query
	 * @return the parsed Query object
	 * @throws ParseException
	 */
	public Query parseQuery(String s) throws ParseException
	{
		Query q = null;
		
		parser.ReInit(new StringReader(s));
		q = parser.Query();
		
		return q;
	}

	/**
	 * Parses a specification file, can be either a prolog or 2apl file. This method is 
	 * typically used to parse a module specification file. Then it is recursively called 
	 * by the parser for each included prolog/module specification file.  
	 * 
	 * @param file the main specification file
	 * @return the parsed APLModule and a list of files (main + includes) that specify 
	 *         this module
	 * @throws ParseModuleException
	 * @throws ParsePrologException
	 */
	public Tuple<APLModule, LinkedList<File>> parseFile(File file)
	throws ParseModuleException, ParsePrologException
	{
		// Create empty module to be filled by the parser
		APLModule a = new APLModule();

		// Create a list of include files, with the first file being the main file
		LinkedList<File> files = new LinkedList<File>();
		files.add(file);

		// The files list is being destructed, keep track of usedFiles
		LinkedList<File> usedFiles = new LinkedList<File>();

		// Obtain path of the main file
		String path = file.getParent() + File.separatorChar;

		if (file.getName().toLowerCase().endsWith(".pl")) 
		{ parsePrologFile(a,file);
		}
		else
		{ try
			{	File included = files.removeFirst();
				while (included != null)
				{ File currentFile = new File(path+included.getName());
				  if (!usedFiles.contains(currentFile))
				  { usedFiles.add(currentFile);
						if (currentFile.toString().toLowerCase().endsWith(".2apl"))
							parseProgram(a, currentFile, files);
						else if (currentFile.toString().toLowerCase().endsWith(".pl"))
							parsePrologFile(a,currentFile);
				  }
				  else
				  { throw( new ParseModuleException(file, "Cyclic or double file inclusion: " + currentFile) );
					}
				  included = files.removeFirst();
				}
		  }
			catch (NoSuchElementException e) {}
		}
		return( new Tuple<APLModule, LinkedList<File>>(a,usedFiles) );
	}

	/**
	 * Parses the actual module specification file. 
	 * 
	 * @param a
	 * @param file
	 * @param files
	 * @throws ParseModuleException
	 */
	private void parseProgram(APLModule a, File file, LinkedList<File> files)
	throws ParseModuleException
	{
	  int BELIEFBASE = 0;
	  int REST = 1;
	  int state = REST;

	  BufferedReader in = null; 

		try 
		{ in = new BufferedReader(new FileReader(file));
		  String belief = "";
			String rest = "";

			String s = in.readLine();
      
			while (s != null)
	    {
				if (s.toLowerCase().trim().startsWith("%")) {
					rest = rest + "\n";
				}
				if (s.toLowerCase().trim().startsWith("beliefs")) {
					state = BELIEFBASE;
					rest = rest + "\n";
				}
				else if (isHeading(s)) {
					state = REST;
					rest = rest + s + "\n";
				}
				else if (state==REST) rest = rest + s + "\n";
				else if (state==BELIEFBASE) {
					belief = belief + s + "\n";
					rest = rest + "\n";
				}
				s = in.readLine();
			}
			
			parser.ReInit(new StringReader(rest));
			
			parser.Program(a,files);
		
			a.getBeliefbase().assertBelief(stripComment(belief));
			in.close();
		}
		catch( IOException e )
		{ try{ in.close(); } catch (Exception ioe) {}
		  throw( new ParseModuleException( file, e.getMessage() ) );
		}
		catch( ParseException e )
		{ try{ in.close(); } catch (IOException ioe) {}
		  if (e.tokenImage != null) {
			 // Default error message
			  throw( new ParseModuleException(file, e) );
		  }
		  else {
			 // Custom error message
			  throw( new ParseModuleException(file, e.getMessage()) );
		  }			  
		}
		catch( JIPSyntaxErrorException e )
		{ throw( new ParseModuleException(file, "Syntax error in belief base\n" + e.getMessage() ) );
		}
		catch( TokenMgrError e )
		{ throw( new ParseModuleException(file, e.getMessage()) );
		}
	}
	
	/**
	 * Removes all the comments occurring in a string.
	 * 
	 * @param s the string to strip
	 * @return the string without comment
	 */
	public static String stripComment(String s)
	{
		String r = "";
		String[] lines = Pattern.compile("$",Pattern.MULTILINE).split(s);
		boolean inBlock = false;
		for (int i=0; i<lines.length; i++)
		{ String a = lines[i].trim();
			
			String c = "";
			for(int j=0; j<a.length(); j++) {
				if (!inBlock&&a.charAt(j)=='/'&&a.charAt(j+1)=='*') {
					inBlock=true;
					j++;
				}
				else if (inBlock&&a.charAt(j)=='*'&&a.charAt(j+1)=='/') {
					inBlock=false;
					j++;
				}
				else if (!inBlock) c = c + a.charAt(j);
			}
			
			int b = c.indexOf("//");
			if (b>-1) c = c.substring(0,b);
						
			r = r + c + "\n";
		}
		return r;
	}
	
	/**
	 * Parses a Prolog file and fills the belief base of the module with the beliefs as
	 * specified by this external prolog file.
	 * 
	 * @param a the module of which the belief base is to be filled
	 * @param file the prolog file to parse
	 * @throws ParsePrologException
	 */
	private void parsePrologFile(APLModule a, File file) throws ParsePrologException
	{
	  try
		{ BufferedReader in = new BufferedReader(new FileReader(file));
		  String belief = "";
		  String s = in.readLine();
		  while (s != null) {
		  	belief = belief + s+"\n";
		  	s = in.readLine();
		  }
		  a.getBeliefbase().assertBelief(stripComment(belief));
		  in.close();
		}
		catch( ParseException e )
		{ throw( new ParsePrologException(file, e ) );
		}
		catch( JIPSyntaxErrorException e )
		{ throw( new ParsePrologException(file, e.getMessage() ) );
		}
		catch( IOException e )
		{ throw( new ParsePrologException(file, e.getMessage() ) );
		}
		catch( TokenMgrError e )
		{ throw( new ParsePrologException(file, e.getMessage()) );
		}
	}

	/**
	 * Parses a multi-agent system file into a list of lines in the mas file. Such a line is
	 * is represented as a list of strings, in which the first element denotes the name of
	 * the agent, the second denotes the name of the specification file and the third until
	 * the n-th denote the names of the environments in which the module is situated. 
	 *  
	 * @param file the MAS specification file
	 * @return the list of mas lines
	 * @throws ParseMASException
	 */
	public ArrayList<ArrayList<String>> parseMas(File file) throws ParseMASException {
	    ArrayList<ArrayList<String>> ret = new ArrayList<ArrayList<String>>();
		
	    	try
			{
			  BufferedReader in = new BufferedReader(new FileReader(file));
				ParserMAS parser = new ParserMAS(in);
				parser.Mas(ret);
			}
			catch( IOException e )
			{ throw( new ParseMASException(file, e.getMessage() ) );
			}
			catch( ParseException e )
			{ throw( new ParseMASException(file, e ) );
			}
			catch( TokenMgrError e )
			{ throw( new ParseMASException(file, e.getMessage()) );
			}

		return ret;
	}

	/** 
	 * Determines whether a string corresponds to a keyword indicating a header 
	 * (start of a base such as belief base).
	 * 
	 * @param s the string to check
	 * @return true if s corresponds to a header, false otherwise
	 */
	private boolean isHeading(String s)
	{
		s = s.toLowerCase().trim();
		return	s.startsWith("beliefupdates")
		||		s.startsWith("goals")
		||		s.startsWith("pcrules")
		||		s.startsWith("pgrules")
		||		s.startsWith("prrules")
		||		s.startsWith("pc-rules")
		||		s.startsWith("pg-rules")
		||		s.startsWith("pr-rules")
		||		s.startsWith("plans")
		||		s.startsWith("belief-update")
		||		s.startsWith("belief update")
		||		s.startsWith("include")
		||		s.startsWith("end");
	}
}
